package com.zenjava.update;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class ZenUpdateManager
{
    private final static Logger LOGGER = Logger.getLogger(ZenUpdateManager.class.getName());

    private static ZenUpdateManager instance;

    public static ZenUpdateManager getInstance()
    {
        if (instance == null)
        {
            instance = new ZenUpdateManager();
        }
        return instance;
    }


    public static final int DEFAULT_UNZIP_BUFFER_SIZE = 1024;
    public static final int DEFAULT_DOWNLOAD_BUFFER_SIZE = 1024;

    private File installedAppProfileFile;
    private int downloadBufferSize;
    private int unzipBufferSize;

    public ZenUpdateManager()
    {
        this(null);
    }

    public ZenUpdateManager(File installedAppProfileFile)
    {
        this.installedAppProfileFile = installedAppProfileFile;
        this.unzipBufferSize = DEFAULT_UNZIP_BUFFER_SIZE;
        this.downloadBufferSize = DEFAULT_DOWNLOAD_BUFFER_SIZE;
    }

    public AppProfile getInstalledAppProfile() throws IOException
    {
        URL url = this.installedAppProfileFile != null
                ? installedAppProfileFile.toURI().toURL()
                : getClass().getResource("/application.xml");

        if (url != null)
        {
            AppProfileReader reader = new AppProfileReader();
            return reader.read(url);
        }
        else
        {
            throw new IOException("No installed app profile (i.e. 'application.xml')");
        }
    }

    public AppProfile checkForUpdate() throws IOException
    {
        AppProfile currentAppProfile = getInstalledAppProfile();
        AppProfileReader reader = new AppProfileReader();
        AppProfile newAppProfile = reader.read(currentAppProfile.getReleaseUrl());
        if (!currentAppProfile.getVersion().equals(newAppProfile.getVersion()))
        {
            return newAppProfile;
        }
        else
        {
            return null;
        }
    }

    public void downloadAndInstallUpdate(AppProfile appProfile, UpdateHandler handler) throws IOException
    {
        // todo look for a delta version from our current version to the new one

        File patchFile = downloadFile(appProfile.getFullUpdateUrl(), handler);
        File baseDir = unzipFile(patchFile, handler);
        patchFile.delete();
        installPatch(handler, baseDir);
    }

    public File downloadFile(URL remoteUrl, UpdateHandler handler) throws IOException
    {
        File localFile = File.createTempFile("zen-", ".update");

        BufferedInputStream reader = null;
        BufferedOutputStream writer = null;
        try
        {
            URLConnection connection = remoteUrl.openConnection();

            handleDownloadUpdate(handler, remoteUrl.getFile(), 0, -1);

            int totalBytesToRead = -1;
            List values = connection.getHeaderFields().get("content-Length");
            if (values != null && !values.isEmpty())
            {
                String fileSizeAsString = (String) values.get(0);
                if (fileSizeAsString != null)
                {
                    try
                    {
                        totalBytesToRead = Integer.parseInt(fileSizeAsString);
                    }
                    catch (NumberFormatException e)
                    {
                        // unable to parse length, just leave it as -1 (indeterminate)
                    }
                }
            }

            InputStream in;
            if ("file".equalsIgnoreCase(remoteUrl.getProtocol()))
            {
                in = new FileInputStream(remoteUrl.getFile());
            }
            else
            {
                in = connection.getInputStream();
            }

            reader = new BufferedInputStream(in);
            writer = new BufferedOutputStream(new FileOutputStream(localFile));

            handleDownloadUpdate(handler, remoteUrl.getFile(), 0, totalBytesToRead);

            byte[] buffer = new byte[downloadBufferSize];
            int totalBytesReadSoFar = 0;
            int bytesRead;
            while ((bytesRead = reader.read(buffer)) > 0)
            {
                writer.write(buffer, 0, bytesRead);
                totalBytesReadSoFar += bytesRead;
                handleDownloadUpdate(handler, remoteUrl.getFile(), totalBytesReadSoFar, totalBytesToRead);
            }

            return localFile;
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                }
                catch (IOException ioe)
                {
                    ioe.printStackTrace();
                }
            }

            if (writer != null)
            {
                try
                {
                    writer.close();
                }
                catch (IOException ioe)
                {
                    ioe.printStackTrace();
                }
            }
        }
    }


    private File unzipFile(File archive, UpdateHandler updateHandler) throws IOException
    {
        // Java doesn't let us create temp dirs easily so instead we create a
        // temp file then delete it and recreate as a directory
        File directory = File.createTempFile("inst", "dir");
        directory.delete();
        directory.mkdir();

        ZipFile zipFile = null;

        try
        {
            zipFile = new ZipFile(archive);
            Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
            byte data[] = new byte[unzipBufferSize];
            int numEntriesProcessed = 0;
            while (zipEntries.hasMoreElements())
            {
                ZipEntry zipEntry = zipEntries.nextElement();
                handleUnpackUpdate(updateHandler, zipEntry.getName(), 0, zipEntry.getSize(), numEntriesProcessed,
                        zipFile.size());

                File file = new File(directory, zipEntry.getName());
                if (zipEntry.isDirectory())
                {
                    file.mkdirs();
                    handleUnpackUpdate(updateHandler, zipEntry.getName(), 1, 1, numEntriesProcessed, zipFile.size());
                }
                else
                {
                    BufferedOutputStream output = null;
                    InputStream zipInput = null;
                    try
                    {
                        file.getParentFile().mkdirs();
                        zipInput = zipFile.getInputStream(zipEntry);
                        output = new BufferedOutputStream(new FileOutputStream(file), unzipBufferSize);

                        handleUnpackUpdate(updateHandler, zipEntry.getName(), 0, zipEntry.getSize(),
                                numEntriesProcessed, zipFile.size());

                        int count;
                        long bytesRead = 0;
                        while ((count = zipInput.read(data, 0, unzipBufferSize)) != -1)
                        {
                            output.write(data, 0, count);
                            bytesRead += count;
                            handleUnpackUpdate(updateHandler, zipEntry.getName(), bytesRead, zipEntry.getSize(),
                                    numEntriesProcessed, zipFile.size());
                        }
                        output.flush();
                        handleUnpackUpdate(updateHandler, zipEntry.getName(), zipEntry.getSize(), zipEntry.getSize(),
                                numEntriesProcessed, zipFile.size());
                    }
                    finally
                    {
                        if (zipInput != null)
                        {
                            zipInput.close();
                        }

                        if (output != null)
                        {
                            output.close();
                        }
                    }
                }
                numEntriesProcessed++;
            }

            handleUnpackUpdate(updateHandler, null, 0, 0, zipFile.size(), zipFile.size());
            return directory;
        }
        finally
        {
            if (zipFile != null)
            {
                zipFile.close();
            }
        }
    }

    private void installPatch(UpdateHandler updateHandler, File baseDir) throws IOException
    {
        handleInstallUpdate(updateHandler);

        // running dir is 'app' - we want parent
        File workingDir = new File("..");

        System.out.println("Working Dir: " + workingDir.getCanonicalPath());

        File patchExecutable = new File(workingDir, "app\\zen-update-patcher.exe");
        ProcessBuilder builder = new ProcessBuilder(
                patchExecutable.getAbsolutePath(),
                baseDir.getAbsolutePath(),
                workingDir.getCanonicalPath()
        );
        builder.directory(workingDir);
        builder.start();
        System.exit(0);
    }

    private void handleDownloadUpdate(UpdateHandler updateHandler,
                                      String fileName,
                                      long downloadedBytesSoFar,
                                      long downloadTotalBytes)
            throws IOException
    {
        if (updateHandler != null)
        {
            ProgressUpdate update = new ProgressUpdate(
                    ProgressUpdate.Stage.downloading, fileName, downloadedBytesSoFar, downloadTotalBytes);
            if (!updateHandler.handleProgressUpdate(update))
            {
                throw new IOException("Cancelled");
            }
        }
    }

    private void handleUnpackUpdate(UpdateHandler updateHandler,
                                    String fileName,
                                    long currentFileBytesReadSoFar,
                                    long currentFileTotalBytes,
                                    int filesUnzippedSoFar,
                                    int totalFilesToUnzip)
            throws IOException
    {
        if (updateHandler != null)
        {
            ProgressUpdate update = new ProgressUpdate(
                    ProgressUpdate.Stage.unpacking, fileName, currentFileBytesReadSoFar, currentFileTotalBytes);
            if (!updateHandler.handleProgressUpdate(update))
            {
                throw new IOException("Cancelled");
            }
        }
    }

    private void handleInstallUpdate(UpdateHandler updateHandler) throws IOException
    {
        if (updateHandler != null)
        {
            ProgressUpdate update = new ProgressUpdate(ProgressUpdate.Stage.installing, null, -1, -1);
            if (!updateHandler.handleProgressUpdate(update))
            {
                throw new IOException("Cancelled");
            }
        }
    }

}
